#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
script usage:

    python javadoc.py [config file path]

config format:

    [Javadoc]
    title = Document
    input_dir = src/
    output_dir = doc/
    java_files = (
        'Apple.java',
        'Banana.java'
        )
    [ExtraOptions]
    sourcepath = src/

    # java files with multiple path
    java_files = {
        'fruit': (
            'Apple.java',
            'Banana.java',
        ),
        'animal': (
            'Cat.java',
            'Dog.java',
        )}

multi-lang support:

    <p lang=zh>
    ...
    </p lang>
    <p lang=en>
    ...
    </p lang>
"""
import os
import re
import shutil
import subprocess
import sys
from functools import partial


if sys.version_info.major <= 2:
    import ConfigParser as configparser
else:
    import configparser


# language that how reference to javadoc locale
LOCALES = {
    'zh': 'zh_CN',
    'en': 'en_US',
}

# expression that how a language start and end
LANG_START  = r'<p\s+lang=(?P<lang>.*?)>'
LANG_END    = r'</p\s+lang>'


def _print(s):
    # print(sys.version_info)
    # print s,         # python 2.x
    # print(s, end='') # python 3.x
    sys.stdout.write(s)


class Util(object):

    @staticmethod
    def isstr(obj):
        return isinstance(obj, basestring)

    @staticmethod
    def isiter(obj):
        # return isinstance(obj, Iterable)
        # str not this attribute in python 2.x
        return hasattr(obj, '__iter__')

    @staticmethod
    def isiter_not_str(obj):
        # http://stackoverflow.com/questions/19943654/type-checking-an-iterable-type-that-is-not-a-string
        return hasattr(obj, '__iter__') and not isinstance(obj, basestring)
        # import collections
        # return not isinstance(obj, basestring) and isinstance(obj, collections.Iterable)

    @staticmethod
    def extend(append, *args):
        assert append
        for arg in args:
            if Util.isiter_not_str(arg):
                for e in arg: append(e)
            else:
                append(arg)

    @staticmethod
    def quote(s):
        return '"' + s + '"';

    @staticmethod
    def next(iter):
        try:
            return iter.next()
        except StopIteration:
            return None

    @staticmethod
    def rmdir(path):
        if os.path.exists(path) and os.path.isdir(path):
            shutil.rmtree(path)

    @staticmethod
    def copyfile(src, dst):
        shutil.copyfile(src, dst)

    @staticmethod
    def copytree(src, dst):
        shutil.copytree(src, dst)

    @staticmethod
    def move(src, dst):
        try:
            shutil.move(src, dst)
        except IOError:
            pass

    @staticmethod
    def mkdirs(path):
        if not os.path.exists(path):
            os.makedirs(path)

    @staticmethod
    def dict2obj(d):
        from collections import namedtuple
        return namedtuple('X', d.keys())(*d.values())

    @staticmethod
    def call(args, **kwargs):
        return subprocess.call(args, **kwargs)

    @staticmethod
    def call_output(args, **kwargs):
        process = subprocess.Popen(args, **kwargs)
        output, unused_err = process.communicate()
        retcode = process.poll()
        return retcode, output


class LangParas(dict):

    def push(self, lang, start, end):
        try:
            self[lang].append((start, end))
        except KeyError:
            self[lang] = [(start, end)]

    def all(self):
        values = []
        for v in self.itervalues():
            for start, end in v:
                values.append(start)
                values.append(end)
        return values

    def iterall(self, sort=True):
        values = self.all()
        if sort: values.sort()
        for i in values:
            yield i

    def paras(self):
        paras = []
        for v in self.itervalues():
            paras.extend(v)
        return paras

    def iterparas(self, sort=True):
        paras = self.paras()
        if sort: paras.sort(key=lambda t: t[0])
        for para in paras:
            yield para

    def others(self, lang):
        paras = []
        for k, v in self.iteritems():
            if k == lang:
                continue
            paras.extend(v)
        return paras

    def iterothers(self, lang, sort=True):
        paras = self.others(lang)
        if sort: paras.sort(key=lambda t: t[0])
        for para in paras:
            yield para


class Parser(object):

    def _reset(self):
        self.langs = set()
        self.result = dict()

    def _readone(self, file):
        paras = LangParas()
        with open(file, 'r') as f:
            lang_start = False
            for i, l in enumerate(f):
                r = re.search(LANG_START, l)
                if r:
                    assert not lang_start, 'Not specify end tag for <p lang>!' \
                        '\nline: %d, %s\nfile: %s' % (start+1, lang_label, file)
                    lang_start = True
                    lang, start = r.group('lang'), i # maybe empty
                    self.langs.add(lang)
                    lang_label = l[r.start():r.end()]
                else:
                    r = re.search(LANG_END, l)
                    if r:
                        assert lang_start, 'Not specify start tag for <p lang>!' \
                            '\nline: %d, %s\nfile: %s' % (i+1, l[r.start():r.end()], file)
                        lang_start = False
                        paras.push(lang, start, i)
            assert not lang_start, 'Not specify end tag for <p lang>!' \
                '\nline: %d, %s\nfile: %s' % (start+1, lang_label, file)
        # print paras, os.path.basename(file)
        if len(paras) > 0:
            self.result[file] = paras

    def read(self, files):
        self._reset()
        for f in files:
            self._readone(f)

    def read_dir(self, input_dir):
        self._reset()
        join = os.path.join
        splitext = os.path.splitext
        for root, dirs, files in os.walk(input_dir):
            for name in files:
                if splitext(name)[1] != '.java':
                    continue
                self._readone(join(root, name))

    def extract(self, lang, files, backup_dir):
        join = os.path.join
        files_new = []
        for f in files:
            output_file = join(backup_dir, f)
            # print input_file, output_file
            Util.mkdirs(os.path.dirname(output_file))
            self._extract(lang, f, output_file)
            files_new.append(output_file)
        return files_new

    def extract_dir(self, lang, input_dir, backup_dir):
        join = os.path.join
        splitext = os.path.splitext
        files_new = []
        for root, dirs, files in os.walk(input_dir):
            for name in files:
                if splitext(name)[1] != '.java':
                    continue
                input_file = join(root, name)
                output_file = join(backup_dir, input_file)
                Util.mkdirs(os.path.dirname(output_file))
                self._extract(lang, input_file, output_file)
                files_new.append(output_file)
        return files_new

    def _extract(self, lang, input_file, output_file):
        if not self.result:
            return
        result = self.result.get(input_file)
        if not result:
            Util.copyfile(input_file, output_file)
            return
        # print 'result', result
        iterall = result.iterall()
        iterothers = result.iterothers(lang)
        line = Util.next(iterall)
        other = Util.next(iterothers)
        with open(input_file, 'r') as src, open(output_file, 'w') as dest:
            for i, l in enumerate(src):
                skip = False
                if line != None and line == i:
                    skip = True
                    line = Util.next(iterall)
                elif other:
                    if other[0] <= i <= other[1]:
                        skip = True
                    elif i > other[1]:
                        other = Util.next(iterothers)
                if not skip:
                    dest.write(l)


class Javadoc(object):

    def __init__(self, cfgfile):
        self.config = self._read_config(cfgfile)

    def clean(self):
        output_dir = self.config.output_dir
        print('clean ' + output_dir)
        Util.rmdir(output_dir)
        self.output_dirs = None
        return self

    def _make(self, outdir, options=None, pkgnames=None, srcfiles=None, cwd=None):
        cmds = ['javadoc']
        extend = partial(Util.extend, cmds.append)

        if options: extend(options)
        extend('-d', outdir)
        if pkgnames: extend(pkgnames)
        if srcfiles: extend(srcfiles)

        if cwd: outdir = os.path.join(cwd, outdir)
        Util.mkdirs(outdir)
        logpath = os.path.join(outdir, 'log')
        with open(logpath, 'w') as log:
            Util.call(cmds, stdout=log, stderr=log, cwd=cwd)
        # Util.call(cmds, stdout=sys.stdout, stderr=sys.stderr, cwd=cwd)

    def make(self):
        join = os.path.join
        cfg = self.config

        options = [
            '-public',
            '-quiet',
            # '-verbose',
        ]
        if cfg.extra_options:
            for k, v in cfg.extra_options:
                options.append('-' + k)
                if v: options.append(v)
        # print(options)

        print('java files: ' + cfg.input_dir)
        print('  + ' + '\n  + '.join(cfg.java_files))
        print

        files = [join(cfg.input_dir, f) for f in cfg.java_files]

        parser = Parser()
        parser.read_dir(cfg.input_dir)

        output_dirs = []

        if parser.langs:
            print('multiple languages: %s' % (','.join(parser.langs)))
            print

            # make multi-lang doc to each language directory
            temp_basedir = join(cfg.output_dir, '.temp')
            for lang in parser.langs:
                temp_dir = join(temp_basedir, lang)
                doc_dir = 'doc_' + lang

                parser.extract_dir(lang, cfg.input_dir, temp_dir)

                # option -locale must be first on the command line
                locale = LOCALES.get(lang, 'en_US')
                options_new = ['-locale', locale]
                if cfg.title: options_new.extend(('-windowtitle', '%s (%s)' % (cfg.title, locale)))
                options_new.extend(options)
                
                output_dir = join(cfg.output_dir, doc_dir)
                print('make doc to ' + output_dir)
                print('     log to ' + join(output_dir, 'log'))

                ret = self._make(doc_dir, options=options_new, srcfiles=files, cwd=temp_dir)

                Util.move(join(temp_dir, doc_dir), output_dir)
                output_dirs.append(output_dir)
            Util.rmdir(temp_basedir)
        else:
            # make defaut doc
            options_new = list(options)
            if cfg.title: options_new.extend(('-windowtitle', cfg.title))

            output_dir = cfg.output_dir
            print('make doc to ' + output_dir)
            print('     log to ' + join(output_dir, 'log'))
            self._make(output_dir, options=options_new, srcfiles=files)
            output_dirs.append(output_dir)

        print
        print('make docs completely')

        self.output_dirs = output_dirs
        return self

    def open(self):
        print('attempt to open docs')
        if self.output_dirs:
            for d in self.output_dirs:
                Util.call(['open', os.path.join(d, 'index.html')])
        return self

    @staticmethod
    def _read_config(cfgfile):
        config = configparser.ConfigParser()
        config.read(cfgfile)
        # javadoc = config.items('Javadoc')
        def opt_get(option, do_eval=False):
            try:
                val = config.get('Javadoc', option)
                return eval(val) if do_eval else val
            except configparser.Error:
                return None
        def opt_java_files():
            java_files = opt_get('java_files', True)
            if isinstance(java_files, dict):
                java_files = [os.path.join(k, i) for k, v in java_files.items() for i in v]
            return java_files
        def opt_extra_options():
            try:
                return config.items('ExtraOptions')
            except configparser.Error:
                return None
        # Reference: http://stackoverflow.com/questions/1305532/
        # convert-python-dict-to-object/
        return Util.dict2obj({
            'title': opt_get('title'),
            'input_dir': opt_get('input_dir'),
            'output_dir': opt_get('output_dir'),
            'java_files': opt_java_files(),
            'extra_options': opt_extra_options(),
        })


def main():
    argv = sys.argv
    cfgfile = 'javadoc.cfg'
    if len(argv) >= 2:
        cfgfile = argv[1]
    if not os.path.exists(cfgfile):
        sys.exit('%s not found' % cfgfile)
    if not os.path.isfile(cfgfile):
        sys.exit('%s not a file' % cfgfile)

    doc = Javadoc(cfgfile)
    # clean output
    doc.clean()
    # make docs
    print
    doc.make()
    # attempt to open
    print
    doc.open()


if __name__ == '__main__':
    main()
